// © 2024 and later: Unicode, Inc. and others.
// License & terms of use: https://www.unicode.org/copyright.html

package com.ibm.icu.dev.test.format;

import com.ibm.icu.dev.test.CoreTestFmwk;
import com.ibm.icu.message2.MessageFormatter;
import com.ibm.icu.text.DateFormat;
import com.ibm.icu.text.DateFormatSymbols;
import com.ibm.icu.text.DateIntervalFormat;
import com.ibm.icu.text.MessageFormat;
import com.ibm.icu.util.TimeZone;
import java.text.FieldPosition;
import java.time.Clock;
import java.time.DayOfWeek;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.Month;
import java.time.OffsetDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.chrono.HijrahDate;
import java.time.chrono.JapaneseDate;
import java.time.chrono.MinguoDate;
import java.time.chrono.ThaiBuddhistDate;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

@RunWith(JUnit4.class)
public class JavaTimeFormatTest extends CoreTestFmwk {
    static final LocalDateTime LDT =
            LocalDateTime.of(
                    /*year*/ 2013,
                    Month.SEPTEMBER,
                    27,
                    /*hour*/ 19, /*min*/
                    43, /*sec*/
                    56, /*nanosec*/
                    123_456_789);

    @Test
    public void testLocalDateFormatting() {
        LocalDate ld = LDT.toLocalDate();

        // Formatting with skeleton
        DateFormat formatFromSkeleton = DateFormat.getInstanceForSkeleton("EEEEyMMMMd", Locale.US);
        assertEquals("", "Friday, September 27, 2013", formatFromSkeleton.format(ld));

        // Format with style
        DateFormat dateFormat = DateFormat.getDateInstance(DateFormat.LONG, Locale.US);
        assertEquals("", "September 27, 2013", dateFormat.format(ld));
    }

    @Test
    public void testLocalDateTimeFormatting() {
        // Formatting with skeleton
        DateFormat formatFromSkeleton =
                DateFormat.getInstanceForSkeleton("EEEEyMMMMd jmsSSS", Locale.US);
        assertEquals(
                "",
                "Friday, September 27, 2013 at 7:43:56.123\u202FPM",
                formatFromSkeleton.format(LDT));

        // Format with style
        DateFormat dateTimeFormat =
                DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.SHORT, Locale.US);
        assertEquals("", "September 27, 2013 at 7:43\u202FPM", dateTimeFormat.format(LDT));

        DateFormat timeFormat = DateFormat.getTimeInstance(DateFormat.SHORT, Locale.US);
        assertEquals("", "7:43\u202FPM", timeFormat.format(LDT));
    }

    @Test
    public void testDayOfWeekFormatting() {
        // Quick sampling
        DateFormat df = DateFormat.getInstanceForSkeleton("EEEE", Locale.FRANCE);
        assertEquals("", "lundi", df.format(DayOfWeek.MONDAY));
        assertEquals("", "mardi", df.format(DayOfWeek.TUESDAY));
        assertEquals("", "jeudi", df.format(DayOfWeek.THURSDAY));
        assertEquals("", "samedi", df.format(DayOfWeek.SATURDAY));
        assertEquals("", "dimanche", df.format(DayOfWeek.SUNDAY));

        // Comprehensive, testing all day-of-week at all widths
        int[] widths = {
            DateFormatSymbols.NARROW,
            DateFormatSymbols.SHORT,
            DateFormatSymbols.ABBREVIATED,
            DateFormatSymbols.WIDE
        };
        String[] widthSkeletons = {"EEEEE", "EEEEEE", "EEE", "EEEE"};
        assertEquals("same size", widths.length, widthSkeletons.length);

        Locale locale = Locale.US;
        DateFormatSymbols dfs = DateFormatSymbols.getInstance(locale);
        for (int i = 0; i < widths.length; i++) {
            String[] expected = dfs.getWeekdays(DateFormatSymbols.FORMAT, widths[i]);
            df = DateFormat.getInstanceForSkeleton(widthSkeletons[i], locale);
            for (DayOfWeek dow : DayOfWeek.values()) {
                int index = dow.getValue();
                // DayOfWeek is 1 based and starting on Monday.
                // DateFormatSymbols day of week starts on Sunday,
                // and the first element in the array empty: [ "", "Su", "Mo", ... "Sa" ]
                index = index % 7 + 1; // convert the index
                assertEquals("java.time.DayOfWeek", expected[index], df.format(dow));
            }
        }
    }

    @Test
    public void testMonthFormatting() {
        // Quick sampling
        DateFormat df = DateFormat.getInstanceForSkeleton("MMMM", Locale.FRANCE);
        assertEquals("", "janvier", df.format(Month.JANUARY));
        assertEquals("", "mars", df.format(Month.MARCH));
        assertEquals("", "mai", df.format(Month.MAY));
        assertEquals("", "juillet", df.format(Month.JULY));
        assertEquals("", "septembre", df.format(Month.SEPTEMBER));
        assertEquals("", "novembre", df.format(Month.NOVEMBER));
        assertEquals("", "décembre", df.format(Month.DECEMBER));

        // Comprehensive, testing all months at all widths
        int[] widths = {
            DateFormatSymbols.NARROW, DateFormatSymbols.ABBREVIATED, DateFormatSymbols.WIDE
        };
        String[] widthSkeletons = {"MMMMM", "MMM", "MMMM"};
        assertEquals("same size", widths.length, widthSkeletons.length);

        Locale locale = Locale.US;
        DateFormatSymbols dfs = DateFormatSymbols.getInstance(locale);
        for (int i = 0; i < widths.length; i++) {
            String[] expected = dfs.getMonths(DateFormatSymbols.FORMAT, widths[i]);
            df = DateFormat.getInstanceForSkeleton(widthSkeletons[i], locale);
            for (Month month : Month.values()) {
                int index = month.getValue() - 1; // java.time.Month is 1 based
                assertEquals("java.time.Month", expected[index], df.format(month));
            }
        }
    }

    @Test
    public void testThatConvertedCalendarUsesDefaultTimeZone() {
        // Save the default time zones
        TimeZone savedTimeZone = TimeZone.getDefault();
        java.util.TimeZone jdkSavedTimeZone = java.util.TimeZone.getDefault();
        // Set to one that we control
        String timeZoneId = "America/New_York";
        TimeZone.setDefault(TimeZone.getTimeZone(timeZoneId));
        java.util.TimeZone.setDefault(java.util.TimeZone.getTimeZone(timeZoneId));

        // We check that the calendar from conversion uses the default time zone.
        DateFormat icuDateFormat =
                DateFormat.getInstanceForSkeleton("EEEEdMMMMyjmszzzz", Locale.US);
        String result = icuDateFormat.format(LDT);

        // Restore the default time zones
        TimeZone.setDefault(savedTimeZone);
        java.util.TimeZone.setDefault(jdkSavedTimeZone);

        assertEquals("", "Friday, September 27, 2013 at 7:43:56 PM Eastern Daylight Time", result);
    }

    @Test
    public void testDateTimeWithTimeZoneFormatting() {
        // Formatting with skeleton
        ZonedDateTime zdt = ZonedDateTime.of(LDT, ZoneId.of("Europe/Paris"));
        OffsetDateTime odt = OffsetDateTime.of(LDT, ZoneOffset.ofHoursMinutes(5, 30));

        DateFormat formatFromSkeleton =
                DateFormat.getInstanceForSkeleton("EEEEyMMMMd jmsSSS vvvv", Locale.US);
        assertEquals(
                "",
                "Friday, September 27, 2013 at 7:43:56.123\u202FPM Central European Time",
                formatFromSkeleton.format(zdt));
        assertEquals(
                "",
                "Friday, September 27, 2013 at 7:43:56.123\u202FPM GMT+05:30",
                formatFromSkeleton.format(odt));

        // Format with style
        DateFormat timeFormat = DateFormat.getTimeInstance(DateFormat.FULL, Locale.US);
        assertEquals("", "7:43:56\u202FPM Central European Summer Time", timeFormat.format(zdt));
        assertEquals("", "7:43:56\u202FPM GMT+05:30", timeFormat.format(odt));

        DateFormat dateTimeFormat =
                DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL, Locale.US);
        assertEquals(
                "",
                "Friday, September 27, 2013 at 7:43:56\u202FPM Central European Summer Time",
                dateTimeFormat.format(zdt));
        assertEquals(
                "",
                "Friday, September 27, 2013 at 7:43:56\u202FPM GMT+05:30",
                dateTimeFormat.format(odt));
    }

    @Test
    public void testNonGregorianDateFormatting() {
        // Non-Gregorian as input
        LocalDate ld = LDT.toLocalDate();
        HijrahDate hd = HijrahDate.from(ld);
        JapaneseDate jd = JapaneseDate.from(ld);
        MinguoDate md = MinguoDate.from(ld);
        ThaiBuddhistDate td = ThaiBuddhistDate.from(ld);

        DateFormat formatFromSkeleton =
                DateFormat.getInstanceForSkeleton("EEEEGGGyMMMMd", Locale.US);
        String expected = "Friday, September 27, 2013 AD";
        assertEquals("", expected, formatFromSkeleton.format(hd));
        assertEquals("", expected, formatFromSkeleton.format(jd));
        assertEquals("", expected, formatFromSkeleton.format(md));
        assertEquals("", expected, formatFromSkeleton.format(td));

        // Non-Gregorian as formatting calendar
        String[] expectedPerCalendar = {
            "buddhist", "September 27, 2556 BE",
            "chinese", "Eighth Month 23, 2013(gui-si)",
            "hebrew", "23 Tishri 5774 AM",
            "indian", "Asvina 5, 1935 Śaka",
            "islamic", "Dhuʻl-Qiʻdah 22, 1434 Anno Hegirae",
            "japanese", "September 27, 25 Heisei",
            "persian", "Mehr 5, 1392 AP",
            "roc", "September 27, 102 Minguo",
        };
        String skeleton = "GGGGyMMMMd";
        for (int i = 0; i < expectedPerCalendar.length; i++) {
            Locale locale = Locale.forLanguageTag("en-u-ca-" + expectedPerCalendar[i++]);
            formatFromSkeleton = DateFormat.getInstanceForSkeleton(skeleton, locale);
            assertEquals("", expectedPerCalendar[i], formatFromSkeleton.format(LDT));
        }
    }

    @Test(expected = IllegalArgumentException.class)
    public void testInstantFormattingFails() {
        DateFormat formatFromSkeleton =
                DateFormat.getInstanceForSkeleton("yMMMMd jmsSSSvvvv", Locale.US);
        Instant instant = LDT.toInstant(ZoneOffset.UTC);
        formatFromSkeleton.format(instant);
    }

    @Test
    public void testMessageFormat() {
        Locale locale = Locale.FRENCH;
        Map<String, Object> arguments = new HashMap<>();
        arguments.put("expDate", LDT);

        // Make sure that the type detection works, we don't pass a type for the formatter
        MessageFormat mf = new MessageFormat("Your card expires on {expDate}", locale);
        assertEquals("", "Your card expires on 27/09/2013 19:43", mf.format(arguments));

        // Now we specify that the placeholder is a date, make sure that the style & skeleton are
        // honored.
        mf = new MessageFormat("Your card expires on {expDate, date}", locale);
        assertEquals("", "Your card expires on 27 sept. 2013", mf.format(arguments));

        mf = new MessageFormat("Your card expires on {expDate, date, FULL}", locale);
        assertEquals("", "Your card expires on vendredi 27 septembre 2013", mf.format(arguments));

        mf = new MessageFormat("Your card expires on {expDate, date, ::EEEyMMMd}", locale);
        assertEquals("", "Your card expires on ven. 27 sept. 2013", mf.format(arguments));

        // MessageFormatter (MF2)

        MessageFormatter.Builder mf2Builder = MessageFormatter.builder().setLocale(locale);

        MessageFormatter mf2 =
                mf2Builder.setPattern("(mf2) Your card expires on {$expDate}").build();
        assertEquals(
                "",
                "(mf2) Your card expires on ven. 27 sept. 2013, 19:43",
                mf2.formatToString(arguments));

        mf2 = mf2Builder.setPattern("(mf2) Your card expires on {$expDate :date}").build();
        assertEquals(
                "", "(mf2) Your card expires on ven. 27 sept. 2013", mf2.formatToString(arguments));

        mf2 =
                mf2Builder
                        .setPattern(
                                "(mf2) Your card expires on {$expDate :date fields=year-month-day length=long}")
                        .build();
        assertEquals(
                "", "(mf2) Your card expires on 27 septembre 2013", mf2.formatToString(arguments));

        mf2 =
                mf2Builder
                        .setPattern(
                                "(mf2) Your card expires on {$expDate :date icu:skeleton=EEEyMMMd}")
                        .build();
        assertEquals(
                "", "(mf2) Your card expires on ven. 27 sept. 2013", mf2.formatToString(arguments));

        // Test several java.time types
        // We don't care much about the string result, as we test that somewhere else.
        // We only want to make sure that MessageFormat(ter) recognizes the types.

        String expectedMf1Result = "Your card expires on ven. 27 sept. 2013";
        String expectedMf2Result = "(mf2) " + expectedMf1Result;
        // LocalDate
        arguments.put("expDate", LDT.toLocalDate());
        assertEquals("", expectedMf1Result, mf.format(arguments));
        assertEquals("", expectedMf2Result, mf2.formatToString(arguments));
        // ZonedDateTime
        arguments.put("expDate", LDT.atZone(ZoneId.of("Europe/Paris")));
        assertEquals("", expectedMf1Result, mf.format(arguments));
        assertEquals("", expectedMf2Result, mf2.formatToString(arguments));
        // OffsetDateTime
        arguments.put("expDate", LDT.atOffset(ZoneOffset.ofHours(2)));
        assertEquals("", expectedMf1Result, mf.format(arguments));
        assertEquals("", expectedMf2Result, mf2.formatToString(arguments));

        // Test that both JDK and ICU Calendar are recognized as types.
        arguments.put("expDate", new java.util.GregorianCalendar(2013, 8, 27));
        assertEquals("", expectedMf1Result, mf.format(arguments));
        assertEquals("", expectedMf2Result, mf2.formatToString(arguments));

        // Make sure that Instant and Clock are not formatted

        // Instant
        Instant instant = LDT.toInstant(ZoneOffset.UTC);
        arguments.put("expDate", instant);
        try {
            mf.format(arguments);
            fail("Should not be able to format java.time.Instant");
        } catch (IllegalArgumentException ex) {
            /* expected to throw */
        }
        try {
            mf2.formatToString(arguments);
            fail("Should not be able to format java.time.Instant");
        } catch (IllegalArgumentException ex) {
            /* expected to throw */
        }

        // Clock
        arguments.put("expDate", Clock.fixed(instant, ZoneId.of("Europe/Paris")));
        try {
            mf.format(arguments);
            fail("Should not be able to format java.time.Clock");
        } catch (IllegalArgumentException ex) {
            /* expected to throw */
        }
        try {
            mf2.formatToString(arguments);
            fail("Should not be able to format java.time.Clock");
        } catch (IllegalArgumentException ex) {
            /* expected to throw */
        }
    }

    @Test
    public void testDateMessageFormatDow() {
        Locale locale = Locale.FRANCE;
        String expected = "wide:lundi abbr:lun. short:lu narrow:L";
        Map<String, Object> arguments = Map.of("dow", DayOfWeek.MONDAY);

        MessageFormat mf =
                new MessageFormat(
                        "wide:{dow,date,::EEEE} abbr:{dow,date,::EEE} short:{dow,date,::EEEEEE} narrow:{dow,date,::EEEEE}",
                        locale);
        assertEquals("", expected, mf.format(arguments));

        MessageFormatter mf2 =
                MessageFormatter.builder()
                        .setPattern(
                                "wide:{$dow :date icu:skeleton=EEEE}"
                                        + " abbr:{$dow :date icu:skeleton=EEE}"
                                        + " short:{$dow :date icu:skeleton=EEEEEE}"
                                        + " narrow:{$dow :date icu:skeleton=EEEEE}")
                        .setLocale(locale)
                        .build();
        assertEquals("", expected, mf2.formatToString(arguments));
    }

    @Test
    public void testDateMessageFormatMonth() {
        Locale locale = Locale.FRANCE;
        String expected = "wide:septembre abbr:sept. narrow:S";
        Map<String, Object> arguments = Map.of("mon", Month.SEPTEMBER);

        MessageFormat mf =
                new MessageFormat(
                        "wide:{mon,date,::MMMM} abbr:{mon,date,::MMM} narrow:{mon,date,::MMMMM}",
                        locale);
        assertEquals("", expected, mf.format(arguments));

        MessageFormatter mf2 =
                MessageFormatter.builder()
                        .setPattern(
                                "wide:{$mon :date icu:skeleton=MMMM}"
                                        + " abbr:{$mon :date icu:skeleton=MMM}"
                                        + " narrow:{$mon :date icu:skeleton=MMMMM}")
                        .setLocale(locale)
                        .build();
        assertEquals("", expected, mf2.formatToString(arguments));
    }

    @Test
    public void testDateIntervalFormat() {
        Locale locale = Locale.FRENCH;
        String intervalSkeleton = "dMMMMy";
        LocalDate from = LocalDate.of(2024, Month.SEPTEMBER, 17);
        LocalDate to = LocalDate.of(2024, Month.SEPTEMBER, 23);
        StringBuffer result = new StringBuffer();

        result.setLength(0);
        DateIntervalFormat di = DateIntervalFormat.getInstance(intervalSkeleton, locale);
        assertEquals(
                "",
                "17–23 septembre 2024",
                di.format(from, to, result, new FieldPosition(0)).toString());

        to = LocalDate.of(2024, Month.OCTOBER, 3);
        result.setLength(0);
        di = DateIntervalFormat.getInstance(intervalSkeleton, locale);
        assertEquals(
                "",
                "17 septembre – 3 octobre 2024",
                di.format(from, to, result, new FieldPosition(0)).toString());

        // LocalDateTime. Date + time difference, same day, different times

        LocalDateTime fromDt = LocalDateTime.of(2024, Month.SEPTEMBER, 17, 9, 30, 0);
        LocalDateTime toDt = LocalDateTime.of(2024, Month.SEPTEMBER, 17, 18, 0, 0);

        result.setLength(0);
        di = DateIntervalFormat.getInstance("dMMMMy jm", locale);
        assertEquals(
                "",
                "17 septembre 2024, 09:30 – 18:00",
                di.format(fromDt, toDt, result, new FieldPosition(0)).toString());

        // LocalDateTime. Time difference, same day

        LocalTime fromT = LocalTime.of(9, 30, 0);
        LocalTime toT = LocalTime.of(18, 0, 0);

        result.setLength(0);
        di = DateIntervalFormat.getInstance("jm", locale);
        assertEquals(
                "",
                "09:30 – 18:00",
                di.format(fromT, toT, result, new FieldPosition(0)).toString());

        // Non-Gregorian output

        di =
                DateIntervalFormat.getInstance(
                        intervalSkeleton, Locale.forLanguageTag("fr-u-ca-hebrew"));
        result.setLength(0);
        assertEquals(
                "",
                "14 éloul – 1 tichri 5785 A. M.",
                di.format(from, to, result, new FieldPosition(0)).toString());

        di =
                DateIntervalFormat.getInstance(
                        intervalSkeleton, Locale.forLanguageTag("fr-u-ca-coptic"));
        result.setLength(0);
        assertEquals(
                "",
                "7 tout – 23 tout 1741 ap. D.",
                di.format(from, to, result, new FieldPosition(0)).toString());

        di =
                DateIntervalFormat.getInstance(
                        intervalSkeleton, Locale.forLanguageTag("fr-u-ca-japanese"));
        result.setLength(0);
        assertEquals(
                "",
                "17 septembre – 3 octobre 6 Reiwa",
                di.format(from, to, result, new FieldPosition(0)).toString());
    }
}
